Auch wenn Frameworks wie Spring Boot aktuell sehr beliebt sind und verbreitet eingesetzt werden, gibt es weiterhin zahlreiche Unternehmen und Projekte, die aus den unterschiedlichsten Gründen ihre Anwendungen auf Basis der APIs implementieren, die als Teil des Java-EE-Standards spezifiziert wurden. Natürlich muss deswegen nicht notwendigerweise ein Java EE Application Server für den Betrieb eingesetzt werden, in vielen Fällen genügen leichtgewichtige Webcontainer wie Tomcat oder Jetty. Aber auch unter denjenigen Containern, die den kompletten Java-EE-Standard unterstützen, finden sich zunehmend schlanke Varianten. Wer sich seit längerer Zeit nicht über den aktuellen technischen Stand der verfügbaren Lösungen informiert hat, sollte dies unbedingt nachholen. Nicht wenige Entwickler zeigen sich überrascht darüber, wie sehr sich die Java-EE-Welt in den vergangenen Jahren gewandelt hat.
Das letzte Release des Enterprise-Standards, das unter altem Namen veröffentlicht wurde, ist Java EE 8. Bekanntlich wurde die Plattform in der Zwischenzeit an die Eclipse Foundation übergeben, wo sie unter dem neuen Namen Jakarta EE [1] weiterentwickelt wird. Mit einem ersten Release von Jakarta EE ist im Laufe dieses Jahres zu rechnen. Allgemein wird erwartet, dass die Plattform fortan in raschem Tempo modernisiert und weiterentwickelt wird. Das MicroProfile- Projekt gibt hierzu einen ersten beeindruckenden Vorgeschmack. Derweil wurde Java EE 8 gemeinhin als Release wahrgenommen, das zwar hier und da kleinere Verbesserungen bringt, im Wesentlichen jedoch keine große Weiterentwicklung oder Modernisierung bedeutet. Speziell für API-Entwickler hat sich jedoch tatsächlich einiges getan. Es lohnt sich daher, einen Blick auf die aktuellen Versionen der relevanten Spezifikationen zu werfen.
STAY TUNED!
Learn more about API Conference
JSON-Unterstützung
Die allermeisten APIs setzen für den Datenaustausch zurzeit auf das JSON-Format. Bislang ist die JSON-Unterstützung der Java-EE-Plattform jedoch recht rudimentär ausgefallen. Mit JSON-P existiert lediglich ein API zum Parsen und Erstellen von JSON-Objekten auf technischem Abstraktionslevel. Was jedoch bislang fehlte, war ein Binding-Mechanismus ähnlich zu JAXB aus der XML-Welt, der eine automatische und transparente Umwandlung zwischen JSON-Konstrukten und (fachlichen) Java-Objekten ermöglicht. Aus diesem Grund kamen in der Vergangenheit in der Regel proprietäre Frameworks zum Einsatz, wie beispielsweise Jackson [2]. Seit dem Release von Java EE 8 steht mit JSON-B nun ein standardisierter Binding-Mechanismus bereit.
JSON-B definiert zunächst einen Regelsatz und somit ein Defaultverhalten für die automatische Umwandlung zwischen JSON und Java. Im Wesentlichen beruht dieses Verhalten auf dem Vergleich der Namen von JSON-Attributen und Felder in Java-Objekten. Im einfachsten Fall müssen Entwickler also gar nichts tun und alles funktioniert wie von selbst. Für alle anderen Fälle standardisiert JSON-B darüber hinaus eine Reihe unterschiedlicher Annotationen, die an fachlichen Java-Klassen bzw. DTOs, ihren Feldern oder Methoden angebracht werden können, um vom Standardfall abweichende Regeln zu hinterlegen. So können beispielsweise abweichende Feldnamen definiert, die Reihenfolge der Felder festgelegt oder Felder bei der Serialisierung ignoriert werden. Weiterhin ist es möglich zu bestimmen, welches Format bei der Umwandlung eines Datums oder einer Uhrzeit angewandt werden soll oder ob Felder mit dem Wert NULL in JSON darzustellen sind oder nicht. Listing 1 zeigt einige Beispiele hierfür.
Listing 1 @JsonbPropertyOrder({"name", "created", "id"}) @JsonbTypeAdapter(RegistrationDetailsAdapter.class) public class RegistrationDetails { @JsonbTransient private Long id; @JsonbDateFormat("dd.MM.yyyy HH:mm:ss") private ZonedDateTime created; @NotNull private String name; public Long getId() { return id; } public void setId(Long id) { this.id = id; } public ZonedDateTime getCreated() { return created; } public void setCreated(ZonedDateTime created) { this.created = created; } @JsonbProperty(value="registration_name") public String getName() { return name; } public void setName(String name) { this.name = name; } }
In manchen Fällen ist eine Eins-zu-eins-Abbildung von Feldern nicht ausreichend. Dies könnte etwa auftreten, wenn eine Klasse Person in Java zwei separate Felder für Vor- und Nachnamen besitzt, für die Darstellung einer Person in JSON jedoch nur ein Feld für Vor- und Nachname vorgesehen ist. In diesem konkreten Fall müsste von zwei Feldern auf ein Feld abgebildet werden oder umgekehrt. Auch für solche Anforderungen hat JSON-B eine Lösung an Bord: Die Implementierung der Schnittstelle JsonbAdapter erlaubt die Implementierung komplexer Abbildungsvorschriften (Listing 2).
Listing 2 public class RegistrationDetailsAdapter implements JsonbAdapter<RegistrationDetails, JsonObject> { @Override public JsonObject adaptToJson(RegistrationDetails details) throws Exception { return Json.createObjectBuilder() .add("registrationId", details.getId()) .add("registrationName", details.getName()) .build(); } @Override public RegistrationDetails adaptFromJson(JsonObject jsonObject) throws Exception { RegistrationDetails details = new RegistrationDetails(); details.setName(jsonObject.getString("name")); return details; } };
Eine weitere wichtige Neuerung im Bereich JSON wurde in JSON-P 1.1 eingeführt, das nun JSON Patch (RFC 6902) [3] unterstützt. Dabei handelt es sich um eine Notation für Patch-Operationen im JSON-Format. Dessen Einsatz wird im nächsten Abschnitt näher erläutert. Darüber hinaus unterstützt JSON-P 1.1 nun auch JSON Merge Patch und JSON Pointer.
Besonders interessant wird die neue, standardisierte JSON-Unterstützung dadurch, dass sie automatisch auch in JAX-RS verfügbar ist. Auf JAX-RS basierende Anwendungen können nun also JSON-Daten versenden oder empfangen, und diese werden von JSON-B hinter den Kulissen in fachliche Java-Objekte umgewandelt.
PATCH
Ein beliebtes Thema für leidenschaftliche Diskussionen unter API-Entwicklern ist die Frage, wie Updates zu implementieren sind. Eine Variante besteht darin, PATCH-Requests einzusetzen. Dieser HTTP-Request-Typ wurde jedoch bislang in JAX-RS nicht unterstützt. In JAX-RS 2.1 wurde dies nun nachgeholt. So wurde für die Serverseite die Annotation @PATCH hinzugefügt, welche analog zu @GET, @PUT, @POST oder @DELETE dazu dient, Ressourcenmethoden zu markieren, an die entsprechende Requests geleitet werden sollen. Listing 3 zeigt, wie JAX-RS, JSON-B und JSON-P kombiniert werden können, um PATCH zu implementieren. JAX-RS liefert die Annotation @PATCH sowie die Möglichkeit, den Payload des Requests als JSON Array entgegenzunehmen, einen Typ, der von JSON-P bereitgestellt wird. Mit Hilfe des JSON-P API kann das Json Array dann in einen JSON Patch umgewandelt werden. Die Anwendung des Patchs wurde in eine separate Methode ausgelagert. Sie ist absichtlich mit vielen Zwischenschritten implementiert, um die Verständlichkeit zu erleichtern. Hier wird zunächst JSON-B verwendet, um das fachliche Objekt in eine JSON-Struktur zu serialisieren, die dann wieder mit JSON-P in ein JSON Object geparst wird. Nun kann mittels JSON-P der Patch auf das JSON Object angewandt werden. Anschließend wird das gepatchte JSON Object wieder in eine JSON-Struktur umgewandelt und diese letztlich an ein fachliches Objekt gebunden.
Listing 3 @PATCH @Path("{id}") @Consumes(MediaType.APPLICATION_JSON_PATCH_JSON) public Response patch(@PathParam("id") Long regId, JsonArray jsonArray) { RegistrationDetails registration = registrationService.getRegistration(regId); JsonPatch patch = Json.createPatch(jsonArray); registration = patch(registration, patch); registrationService.updateRegistration(regId, registration); return Response.noContent().build(); }; private RegistrationDetails patch(RegistrationDetails details, JsonPatch patch){ Jsonb jsonb = JsonbBuilder.create(); // covert POJO to JsonObject String detailsAsJsonString = jsonb.toJson(details); JsonObject detailsAsJsonObject = Json.createReader(new StringReader(detailsAsJsonString)).readObject(); // patch JsonObject with JSON-P JsonObject patchedDetailsAsJsonObj = patch.apply(detailsAsJsonObject); // convert back to POJO details = jsonb.fromJson(patchedDetailsAsJsonObj.toString(), RegistrationDetails.class); return details; }
How to develop Web APIs?
Explore the API Development Track
Währenddessen enthält das JAX-RS Client-API jedoch weiterhin keine Unterstützung für PATCH. Dies erscheint zunächst überraschend, hat jedoch nachvollziehbare Gründe: JAX-RS 2.1 wurde als Teil von Java EE 8 spezifiziert, das Kompatibilität zu Java SE 8 garantiert. In Java SE 8 war jedoch standardmäßig die Klasse HttpUrlConnection für den Versand von HTTP-Requests vorgesehen. Diese sehr alte Klasse stammt noch aus Zeiten von Java 1.1 (!) und unterstützt kein HTTP PATCH. Zwar könnten JAX-RS-Implementierungen theoretisch alternative HTTP-Clients verwenden, zum Beispiel die verbreiteten HttpClients aus dem Apache-HttpComponents-Projekt. Da jedoch auf Ebene des JAX-RS API keine Annahme über solche Implementierungsdetails gemacht werden kann, musste zunächst von HttpUrlConnection ausgegangen werden. Somit ergab es keinen Sinn, eine Unterstützung für PATCH in das JAX-RS Client-API aufzunehmen, wenn der Standardfall gewesen wäre, dass die zugrunde liegende HTTP-Client-Implementierung gar keine PATCH Requests unterstützt. In der Zwischenzeit wurde mit Java SE 12 aber ein neuer, moderner HttpClient in den Sprachstandard aufgenommen, der nicht nur PATCH, sondern insbesondere auch HTTP/2 unterstützt, sodass das nächste Release von JAX-RS sicherlich auch auf Clientseite den Versand von PATCH Requests unterstützen wird. Bis dahin müsste auf ein alternatives Framework für Java-basierte REST-Clients zurückgegriffen werden, wenn PATCH Requests versendet werden sollen.
Die Kombination der neuen Unterstützung von JSON und HTTP PATCH in JAX-RS 2.1 sowie von JSON Patch in JSON-P erlauben es nun beispielsweise auch, APIs zu implementieren, die nicht die zu aktualisierenden Daten selbst, sondern PATCH-Anweisungen im Format JSON Patch übertragen. Tabelle 1 zeigt den Unterschied. Zu beachten sind insbesondere auch die unterschiedlichen Media-Types.
Ohne JSON Patch | Mit JSON Patch |
PATCH /api/persons/xHyFYW2pQb HTTP/1.1 Content-Type=application/json { “lastName”: “Becker”, “address”: { “street”: “Mainzer Landstraße 4” } } |
PATCH /api/persons/xHyFYW2pQb HTTP/1.1 Content-Type=application/json-patch+json [ { “op”: “replace”, “path”: “/lastName”, “value”: “Becker” }, { “op”: “replace”, “path”: “/address/street”, “value”: “Mainzer Landstraße 4” } ] |
Tabelle 1: PATCH mit/ohne JSON Patch
Server-sent Events
Eine verbreitete Herausforderung bei der Implementierung von Webanwendungen besteht darin, die Clients zeitnah über Ereignisse oder Datenänderungen zu informieren, die auf der Serverseite eingetreten sind. Anschauliche Beispiele hierfür sind etwa Webangebote, die aktuelle Spielstände von Sportereignissen oder Aktienkurse anzeigen, also Daten, die sich jederzeit verändern können. Es sollte natürlich gewährleistet sein, dass die Clients solcher Anwendungen möglichst keine veralteten Stände anzeigen. Eine einfache, jedoch gleichzeitig ineffiziente Möglichkeit besteht in der Implementierung eines Polling-Mechanismus. Dabei fragen die Clients in regelmäßigen, typischerweise kurzen Abständen wiederholt den aktuellen Stand beim Server ab. Ändern sich die Daten nur selten, kann auf diese Weise sehr leicht eine erhebliche unnötige Last auf Netzwerk und Anwendungsserver entstehen. Über die Jahre sind daher unterschiedliche alternative Lösungen entstanden. Eine dieser Varianten sind sogenannte Push-Verfahren, bei denen die Initiative nicht vom Client ausgeht, sondern vom Server. Dieser sendet (pusht) Daten nur dann zu den Clients, wenn sie sich verändert haben oder ein relevantes Ereignis eingetreten ist. Die Vielzahl unnötiger, von den Clients initiierter Anfragen, ob sich Daten verändert haben, entfällt somit.
Bei Server-sent Events (SSE) handelt es sich um einen offiziellen Standard, der 2015 vom W3C (World Wide Web Consortium) verabschiedet wurde [4] und inzwischen von der WHATWG (Web Hypertext Application Technology Working Group) gepflegt wird [5]. Er ermöglicht die Umsetzung genau solcher Push-Verfahren auf Basis von HTTP und langlebiger Verbindungen. Der Standard definiert hierzu unter anderem einen speziellen MIME-Typ (text/event-stream) und ein API, mit Hilfe derer sich Clients an einer SSE-Quelle für den Empfang von Server-sent Events registrieren können, um fortan die jeweiligen Ereignisse zu empfangen. Im Unterschied zu anderen Lösungen handelt es sich bei Server-sent Events um eine unidirektionale Verbindung. Das SSE API wird vor allen Dingen von Browsern unterstützt, sodass Webanwendungen mittels JavaScript-Code auf SSE-Quellen zugreifen können. Unabhängig davon sind aber auch Implementierungen des API für beliebige andere Programmiersprachen denkbar. JAX-RS 2.1 unterstützt sowohl die Implementierung von SSE-Quellen (Listing 4) als auch von SSE-Clients in Java.
Listing 4 @Singleton @Path("sse/broadcast") public class SseResource { @Context private Sse sse; private SseBroadcaster sseBroadcaster; private AtomicLong ids = new AtomicLong(); @PostConstruct public void init() { sseBroadcaster = sse.newBroadcaster(); } @GET @Produces(MediaType.SERVER_SENT_EVENTS) public void subscribe(@Context SseEventSink sink) { sseBroadcaster.register(sink); Long sinkId = ids.incrementAndGet(); sink.send(sse.newEvent("Welcome, you are registered! ID=" + sinkId)); } @POST @Consumes(MediaType.MULTIPART_FORM_DATA) public void broadcast(@FormParam("event") String eventtext) { sseBroadcaster.broadcast(sse.newEvent(eventtext)); } };
Ein typischer Einsatzzweck des SSE-Supports von JAX-RS wird sicherlich sein, eine SSE-Quelle zu implementieren. Wird beispielsweise eine Single Page Application (SPA) erstellt, die mit einem in Java implementierten Backend API kommuniziert, so könnte das Frontend dieser Anwendung nun mit Hilfe von SSE über Ereignisse informiert werden, die im Backend auftreten. Arbeiten beispielsweise hundert Benutzer gleichzeitig an einem solchen System und ändert oder erstellt einer dieser Benutzer einen bestimmten Datensatz mit einem POST, PUT oder PATCH Request, dann könnte diese Änderung anschließend via SSE Broadcast an alle anderen 99 Benutzer gesendet werden, sodass diese zeitnah über den neuen Stand informiert werden.
Es ist wichtig zu verstehen, dass SSE keine Garantien über die Auslieferung von Events übernimmt. Events können also verloren gehen, und wenn ein Client vorübergehend nicht verbunden ist (offline oder Neustart), wird er während dieser Zeit auftretende Events verpassen. Server-sent Events eignen sich daher nur für bestimmte Anwendungsfälle, bei denen das Verpassen von Events unkritisch ist.
Server-sent Events werden von allen gängigen Browsern unterstützt – mit Ausnahme von Internet Explorer und Edge. Aus diesem Grund sollte ein Einsatz in öffentlich zugänglichen Webanwendungen genau überdacht werden. Da die beiden Browservarianten von Microsoft eine recht hohe Verbreitung haben, besteht die Gefahr, einen großen Teil von Benutzern oder Kunden auszuschließen, falls wichtige Funktionen der Webanwendung auf Server-sent Events aufbauen. Gegebenenfalls kann man aber für Benutzer dieser Browser eine Alternative anbieten, um die Anwendungen dennoch benutzen zu können. Ganz anders schaut es natürlich bei Webanwendungen aus, die für einen geschlossenen Benutzerkreis erstellt werden, z. B. unternehmensinterne Anwendungen. In solchen Fällen lässt sich oft vorgeben, welchen Browser die Benutzer verwenden.
Auch das JAX-RS Client-API wurde für den Einsatz von Server-sent Events erweitert, sodass Events auch an Anwendungen gepusht werden können, die in Java implementiert wurden. Diese Art des Einsatzes wird aber vermutlich eher seltener benötigt werden.
Reactive API-Clients
Bei der Entwicklung von API-Clients gibt es sehr häufig Anwendungsfälle, bei denen eine mehrstufige Logik und somit mehrere aufeinanderfolgende Requests zu implementieren sind. Ein beispielhaftes Szenario ist die Verwendung des Google Maps API [6]. So könnte in einem ersten Schritt eine von einem Benutzer erfasste Adresse an das API gesandt werden, um diese zu validieren, in eine standardisierte Schreibweise zu überführen und die entsprechenden Geokoordinaten zu ermitteln. Mit Hilfe der Geokoordinaten könnten dann in den folgenden Schritten und weiteren Requests zusätzliche Informationen zu dieser Adresse ermittelt werden, etwa die Anzahl von Banken, Schulen, Restaurants und Einkaufsmöglichkeiten im Umkreis, oder die Fahrzeiten zum nächsten Flughafen oder zur Autobahn. Idealerweise werden solche API Requests asynchron ausgeführt, was auch bislang schon mit JAX-RS möglich war. Umständlich war aber die Fehlerbehandlung. So musste für jeden asynchron abgesetzten Request ein InvocationCallback definiert werden, der eventuelle Fehlerfälle behandelt, im Erfolgsfall aber die nächste Stufe der Anwendungslogik ausführt, also den nächsten Request abgeschickt – der wiederum einen InvocationCallback für beide Fälle benötigt. Bei der Implementierung einer solchen mehrstufigen Logik ergab sich somit eine mehrfache Verschachtelung von InvocationCallback, resultierend in schlecht lesbarem und wartbarem Code.
Zum Glück bietet Java SE seit einiger Zeit Abhilfe für solche Anforderungen in Form der Schnittstelle CompletionStage und ihrer Implementierung CompletableFuture. Diese erlauben es ganz generell, mehrstufige Fachlogik inklusive der notwendigen Fehlerbehandlung leicht lesbar und verständlich zu formulieren. Das Ergebnis einer vorherigen Stufe wird dabei im Erfolgsfall automatisch an die nächste Stufe geleitet, sobald es vorliegt. Entwickler müssen sich also nicht mehr selbst darum kümmern, auf Ergebnisse zu warten, diese abzuholen und weiterzuleiten. Eine auf Basis von CompletionStage implementierte Abfolge von Schritten kann dann asynchron mit Hilfe eines oder mehrerer Threads aus einem Threadpool ausgeführt werden.
In JAX-RS 2.1 wurde das Client-API um eine reaktive Variante erweitert, die beim Versand asynchroner Requests eine CompletionStage zurückliefert. Hierfür ist lediglich die Methode rx() anstelle von asynch() aufzurufen, die Implementierung von InvocationCallback ist dann nicht mehr notwendig. Stattdessen können die weiteren Schritte und auch die Fehlerbehandlung auf Basis der CompletionStage implementiert werden [7]. Zudem wurde das JAX-RS API erweiterbar ausgelegt, sodass JAX-RS-Implementierungen optional auch alternative reaktive Frameworks unterstützen können. Die JAX-RS-Referenzimplementierung Jersey unterstützt beispielsweise zusätzlich das beliebte RxJava.
Fazit
Entgegen der verbreiteten Meinung, das Release von Java EE 8 hätte nur wenige interessante Neuigkeiten gebracht, zeigt sich bei genauerem Hinsehen, dass insbesondere für API-Entwickler tatsächlich doch einige spannende Neuerungen dabei sind, die entweder das Leben leichter machen oder sogar ganz neue Möglichkeiten bieten. Vieles davon konnte auch zuvor schon mit Hilfe proprietärer Frameworks umgesetzt werden. Vom Einsatz standardisierter APIs versprechen sich jedoch nicht wenige Unternehmen eine gewisse Stabilität und Investitionssicherheit. Neben den hier vorgestellten Neuigkeiten in JAX-RS und im JSON-Bereich ist unbedingt noch das neue Security-API zu erwähnen, das es erstmals ermöglicht, eigene Authentifizierungsmechanismen mit Hilfe standardisierter APIs zu implementieren. Bisher waren hierzu entweder herstellerspezifische, also proprietäre Schnittstellen vonnöten, oder der Einsatz von JASPIC, an das jedoch nur sehr wenige Entwickler mit Freude zurückdenken. Auf Basis des neuen Security-API ließe sich dagegen mit nur sehr wenig Aufwand beispielsweise eine auf JSON Web Token basierende Implementierung realisieren, die einfach mittels CDI in die Anwendung integriert wird. Höchste Zeit also, all dies in der Praxis auszuprobieren. Viel Spaß dabei!
Links & Literatur
[1] Jakarta EE: https://jakarta.ee/
[2] Jackson: https://github.com/FasterXML/jackson
[3] JSON Patch: https://tools.ietf.org/html/rfc6902
[4] Server-sent Events (W3C): https://www.w3.org/TR/eventsource/
[5] Server-sent Events (WHATWG): https://html.spec.whatwg.org/multipage/server-sent-events.html#server-sent-events
[6] Google Maps API: https://developers.google.com/maps/documentation/